import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';

import '../src/callback/impl.dart';
import '../src/constants/constants.dart';
import '../src/db/task_entity.dart';
import 'constants/task_status.dart';
import 'db/sqflite_helper.dart';
import 'db/task_provider.dart';
import 'upload/io.dart' if (dart.library.html) 'src/html.dart';
import '../src/utils/exception_ext.dart';
import '../src/utils/file_utils.dart';
import '../src/utils/int_ext.dart';

class FileUploader {
  static final FileUploader _instance = FileUploader._internal();

  factory FileUploader() {
    return _instance;
  }

  late Dio _client;
  final cancelTokens = <String, CancelToken>{};
  final callbacks = <String, UploadCallback>{};
  final ValueNotifier<Map> notifier =
      ValueNotifier<Map<String, UploadCallback>>({});
  UploadCallback? callback;
  Map<String, dynamic>? headers;
  late UploaderCore _upload;
  late TaskProvider provider;
  final running = <TaskEntity>[];

  FileUploader._internal() {
    // BaseOptions、Options、RequestOptions 都可以配置参数，优先级别依次递增，且可以根据优先级别覆盖参数
    BaseOptions options = BaseOptions(
      connectTimeout: Constants.connectTimeout,
      sendTimeout: Constants.connectTimeout,
      // 响应流前后两次接受到数据的间隔，单位为毫秒。
      receiveTimeout: Constants.receiveTimeout,
    );

    _client = Dio(options);
    _upload = UploaderCore(_client);
    provider = TaskProvider.instance;
  }

  initialize() async {
    await SqfliteHelper.instance.ready;
  }

  registerCallback({UploadCallback? callback}) {
    this.callback = callback;
  }

  retry(String taskId) {}

  // required String taskId,
  // required String status,
  // String? group,
  //     Map<String, dynamic>? file,
  // double? progress,
  //     String? error,

  static Future<MultipartFile> _createMultiPartFile(String filePath) async =>
      await MultipartFile.fromFile(filePath);

  ///
  /// 加入队列
  /// [metaData] 元数据
  /// [url] 上传地址
  /// [filePath] 文件路径
  /// [successMessage] 成功消息
  /// [errorMessage] 错误消息
  /// [maxChunkSize] 最大分片大小
  /// [group] 分组
  /// [callback] 回调
  /// [headers] 请求头
  /// [cancelToken] 取消token
  ///
  Future<String> enqueued({
    required String url,
    required String filePath,
    required String fileName,
    int maxChunkSize = 1,
    String? group,
    Map<String, dynamic>? headers,
    CancelToken? cancelToken,
  }) async {
    if (headers != null) {
      _client.options.headers.addAll(headers);
    }
    final taskId = const Uuid().v4();
    final file = File(filePath);
    final task = TaskEntity()
      ..filePath = filePath
      ..fileName = fileName
      ..fileSize = file.lengthSync()
      ..url = url
      ..group = group ?? 'default'
      ..createdAt = DateTime.now().millisecondsSinceEpoch
      ..status = TaskStatus.enqueued.name
      ..progress = 0.0
      ..taskId = taskId;
    cancelTokens[taskId] = cancelToken ?? CancelToken();
    await provider.insertTask(task: task);
    uploadFromFilePath(
      task: task,
      maxChunkSize: maxChunkSize,
      group: group,
      headers: headers,
      cancelToken: cancelTokens[taskId],
    );
    return taskId;
  }

  /// Uploads a multi-part file from a specified URL.
  ///
  /// This function initiates an HTTP POST request to upload a multi-part file and its associated metadata to a specified URL.
  /// It is designed to handle files that have been prepared as multi-part files.
  ///
  /// [metaData]: Metadata associated with the file, such as the file name and size. This parameter is optional.
  /// [signedUrl]: The signed URL for uploading the file, which includes authentication information.
  /// [filePath]: The local path of the file to be uploaded.
  /// [successMessage]: A custom success message after successful upload. This parameter is optional.
  /// [errorMessage]: A custom error message for failed upload. This parameter is optional.
  ///
  /// Returns a Future<bool>, indicating whether the upload was successful.
  Future<void> uploadFromFilePath({
    required TaskEntity task,
    int maxChunkSize = 1,
    String? group,
    Map<String, dynamic>? headers,
    CancelToken? cancelToken,
  }) async {
    if (headers != null) {
      _client.options.headers.addAll(headers);
    }
    try {
      // final response = await _client.post(url, data: formData);
      final response = await _upload.fromFilePath(
        task: task,
        cancelToken: cancelToken,
        maxChunkSize: maxChunkSize,
        onUploadProgress: (task, progress) async {
          task.progress = progress;
          callback?.call(
            task.taskId,
            task.status,
            task.progress ?? 0.0,
          );
          await provider.updateTask(task: task);
        },
        method: 'POST',
        fileKey: 'file',
      );
      if (response?.statusCode != 200) {
        // final newTask = provider.fetchTaskWithId(taskId: task.taskId);
        if (task.errorCode == TaskStatus.paused.name) {
          task.status = TaskStatus.paused.name;
        } else {
          task.status = TaskStatus.failed.name;
        }
        task
          ..errorMessage = response?.statusMessage
          ..errorCode = response?.statusCode.toString();
        debugPrint('uploadFromFilePath error: ${response?.statusMessage}');
        callback?.call(
          task.taskId,
          task.status,
          task.progress ?? 0.0,
          data: null,
          error: task.errorMessage,
        );
        await provider.updateTask(task: task);
      } else {
        task
          ..status = TaskStatus.complete.name
          ..progress = 1.0;

        callback?.call(
          task.taskId,
          task.status,
          task.progress ?? 1.0,
          data: response?.data,
          error: null,
        );
        await provider.updateTask(task: task);
      }

      // Determine if the request was successful based on the status code
    } on DioException catch (e) {
      // Return false in case of a network exception
      debugPrint('is DioException');
      if (e.type == DioExceptionType.cancel) {
        task.status = TaskStatus.paused.name;
        task.errorCode = TaskStatus.paused.name;
        debugPrint(task.errorCode);
      }
      task.errorMessage = e.handleException();
      callback?.call(
        task.taskId,
        task.status,
        task.progress ?? 0.0,
        data: null,
        error: task.errorMessage,
      );
      debugPrint(e.toString());
    }
  }

  /// Uploads file bytes to a specified URL.
  ///
  /// This function uploads the raw bytes of a file to a specified URL using an HTTP PUT request, suitable for smaller files or when only file bytes need to be uploaded.
  ///
  /// [signedUrl]: The signed URL for uploading the file, which includes authentication information.
  /// [pickedFile]: The local file object to be uploaded.
  /// [successMessage]: A custom success message after successful upload. This parameter is optional.
  /// [errorMessage]: A custom error message for failed upload. This parameter is optional.
  ///
  /// Returns a Future<bool>, indicating whether the upload was successful.
  Future<bool> uploadFileBytes({
    required String url,
    required File pickedFile,
    String? successMessage,
    String? errorMessage,
  }) async {
    try {
      // Get the file bytes directly
      final fileBytes = await pickedFile.readAsBytes();

      // Get the file extension and content type
      final String? fileExtension = FileUtils.getExtension(pickedFile.path);
      final String contentType = FileUtils.getContentType(fileExtension);

      // Execute the PUT request and get the response, while setting the content type and validation rules
      final response = await _client.put(
        url,
        data: fileBytes,
        options: Options(headers: {
          'Content-Type': contentType,
        }, validateStatus: (status) => status != null && status.isSuccessful),
      );
      return (response.statusCode ?? 500).isSuccessful;
    } on DioException catch (e) {
      // Return false in case of a network exception
      return false;
    }
  }

  cancelTask(String taskId) async {
    cancelTokens[taskId]?.cancel();
    final task = await provider.fetchTaskWithId(taskId: taskId);
    if (task != null) {
      task.status = TaskStatus.canceled.name;
      await provider.updateTask(task: task);
    }
  }

  pauseTask(String taskId) async {
    cancelTokens[taskId]?.cancel();
    await provider.updateStatusWithTaskId(
      taskId: taskId,
      status: TaskStatus.paused.name,
    );
  }

  Future<int> resumeTask(String taskId) async {
    cancelTokens[taskId]?.cancel();
    final task = await provider.fetchTaskWithId(taskId: taskId);
    if (task == null) {
      return 0;
    }
    cancelTokens[taskId] = CancelToken();
    uploadFromFilePath(
      task: task,
      cancelToken: cancelTokens[taskId],
    );
    provider.updateStatusWithTaskId(
      taskId: taskId,
      status: TaskStatus.enqueued.name,
    );
    return 1;
  }
}
